Conversation
c04d47c to
0f1b15b
Compare
0f1b15b to
b664dd4
Compare
PhilippGackstatter
left a comment
There was a problem hiding this comment.
Looks good, thank you! I primarily looked at the Rust code for now and left a few comments, but on a high level:
- I think we should use the builder-style way of creating
PswapNoteandPswapNoteStorage, since both have quite a few fields. - Some lints are failing (see CI or run
make lintlocally). - If this PR is ready for review, can we put it ouf of draft mode?
- Nits: Would be nice to use attachment instead of aux, storage instead of inputs, sender instead of creator, etc.
| pub fn create<R: FeltRng>( | ||
| creator_account_id: AccountId, | ||
| offered_asset: Asset, | ||
| requested_asset: Asset, | ||
| note_type: NoteType, | ||
| note_attachment: NoteAttachment, | ||
| rng: &mut R, | ||
| ) -> Result<Note, NoteError> { |
There was a problem hiding this comment.
As motivated in #2283, I think we should get rid of this method and steer users towards using the builder methods to create notes.
This requires adding a public build method which we could do like this:
// Use a custom build method for custom validation.
impl<S: IsComplete> PswapNoteBuilder<S> {
pub fn build(self) -> Result<PswapNote, NoteError> {
let note = self.build_internal();
// TODO: Validate note if needed.
// We should probably validate the number of assets here, for instance.
Ok(note)
}
}…MS, remove create_ wrappers, add builder finish_fn - Rename swapp_tag -> pswap_tag and SWAPp -> PSWAP throughout - Rename NUM_ITEMS -> NUM_STORAGE_ITEMS for clarity - Remove create_p2id_payback_note and create_remainder_note wrappers, make build_ functions public instead - Compute p2id_tag inside build_p2id_payback_note from self.storage - Add #[builder(finish_fn(vis = "", name = build_internal))] to PswapNote
…ctly Replace all test helper wrappers with direct calls to library functions: - create_pswap_note -> PswapNote::create() - create_expected_pswap_p2id_note + create_expected_pswap_remainder_note -> pswap.execute() - build_pswap_storage -> PswapNoteStorage::from_parts() - Remove make_pswap_tag, make_note_assets, make_note_args, compute_p2id_tag_* - Inline calculate_output_amount as PswapNote::calculate_output_amount()
- Replace storage layout list with markdown table - Remove trivial "Returns the X" docs on simple getters - Add # Errors sections where relevant - Rewrite method docs to describe intent, not implementation - Add one-line docs on From/TryFrom conversion impls - Tighten PswapNote struct doc
- Rename RpoRandomCoin to RandomCoin (miden-crypto 0.23 rename) - Store full ASSET_KEY instead of prefix/suffix to preserve callback metadata in faucet_id_suffix_and_metadata - Replace create_fungible_asset calls with direct ASSET_KEY + manual ASSET_VALUE construction, avoiding the new enable_callbacks parameter - Update hardcoded P2ID script root to match current P2idNote::script_root()
- Replace hardcoded P2ID script root with procref.p2id::main for compile-time resolution - Default to full fill when both input and inflight amounts are zero - Replace magic address 4000 with named P2ID_RECIPIENT_STORAGE constant - Remove step numbering from comments, fix memory layout docs
- Rename P2ID_RECIPIENT_STORAGE to P2ID_RECIPIENT_SUFFIX/PREFIX - Add METADATA_HEADER word layout docs with source reference - Document attachment_scheme parameter in set_word_attachment call - Add stack views after condition checks
…lper, simplify build_p2id_payback_note Rename requested_key/requested_value/requested_amount to requested_asset_key/requested_asset_value/requested_asset_amount for clarity. Extract offered_asset_amount() helper on PswapNote to deduplicate offered asset extraction. Simplify build_p2id_payback_note to take fill_amount: u64 instead of aux_word: Word, constructing the aux word internally.
- Use NoteTag::default() instead of NoteTag::new(0) - Change swap_count from u64 to u16 with safe try_into conversion - Rename p2id_tag to payback_note_tag, p2id_payback_note to payback_note - Rename build_ prefix to create_ for consistency - Rename aux_word to attachment_word - Replace Felt::new with Felt::from where possible - Rename inputs to note_storage in TryFrom impl - Make create_payback_note and create_remainder_pswap_note private - Add offered_faucet_id helper to replace unreachable!()
- Remove PswapNote::create, add public build() with validation on builder - Add bon::Builder to PswapNoteStorage, remove new() and from_parts() - Remove payback_note_tag field, compute from creator_account_id on the fly - Fix clippy warnings (useless conversions, needless borrows) - Update all unit and integration tests to use builder pattern
- Compare full faucet_id instead of just prefix in build validation - Rename swap_note to pswap_note in integration tests for consistency - Extract PswapNoteStorage builder into separate let bindings - Replace manual current_swap_count counter with enumerate index - Fix clippy useless_conversion and needless_borrow warnings
5eb3cc0 to
1780388
Compare
PhilippGackstatter
left a comment
There was a problem hiding this comment.
Thanks for the updates! Looks good overall.
I left a few comments regarding the core logic in Rust. I will take a look at the MASM implementation next.
| # PSWAP Note Storage (18 items loaded at address 0) | ||
| # REQUESTED_ASSET_WORD_INPUT is the base address of an 8-cell block: | ||
| # addresses 0x0000-0x0003 = ASSET_KEY, 0x0004-0x0007 = ASSET_VALUE | ||
| const REQUESTED_ASSET_WORD_INPUT = 0x0000 | ||
| const REQUESTED_ASSET_VALUE_INPUT = 0x0004 | ||
| const SWAPP_TAG_INPUT = 0x0008 | ||
| const P2ID_TAG_INPUT = 0x0009 | ||
| const SWAPP_COUNT_INPUT = 0x000C | ||
| const SWAPP_CREATOR_PREFIX_INPUT = 0x0010 | ||
| const SWAPP_CREATOR_SUFFIX_INPUT = 0x0011 |
There was a problem hiding this comment.
Nit: These are called inputs, but note inputs were renamed to note storage, so this no longer fits that well. I'd suggest removing the _INPUT or replacing it with _ITEM.
There was a problem hiding this comment.
Yes, make sense. I will change it to Item.
| # PSWAP script expects exactly 18 note storage items | ||
| const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP wrong number of inputs" | ||
|
|
||
| # PSWAP script requires exactly one note asset | ||
| const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP wrong number of assets" |
There was a problem hiding this comment.
| # PSWAP script expects exactly 18 note storage items | |
| const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP wrong number of inputs" | |
| # PSWAP script requires exactly one note asset | |
| const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP wrong number of assets" | |
| # PSWAP script expects exactly 18 note storage items | |
| const ERR_PSWAP_WRONG_NUMBER_OF_INPUTS="PSWAP script expects exactly 18 note storage items" | |
| # PSWAP script requires exactly one note asset | |
| const ERR_PSWAP_WRONG_NUMBER_OF_ASSETS="PSWAP script requires exactly one note asset" |
Nit: More helpful error messages.
| // Ensure offered asset exists and is fungible | ||
| if self.assets.num_assets() != 1 { | ||
| return Err(NoteError::other("Swap note must have exactly 1 offered asset")); | ||
| } |
There was a problem hiding this comment.
We're checking this condition in build now, so this seems redundant.
| /// Extracts the faucet ID of the requested asset from the key word. | ||
| pub fn requested_faucet_id(&self) -> Result<AccountId, NoteError> { | ||
| AccountId::try_from_elements(self.requested_asset_key[2], self.requested_asset_key[3]) | ||
| .map_err(|e| NoteError::other_with_source("failed to parse faucet ID from key", e)) | ||
| } | ||
|
|
||
| /// Extracts the requested token amount from the value word. | ||
| pub fn requested_asset_amount(&self) -> u64 { | ||
| self.requested_asset_value[0].as_canonical_u64() | ||
| } | ||
| } |
There was a problem hiding this comment.
Can we define PswapNoteStorage as:
pub struct PswapNoteStorage {
requested_asset: FungibleAsset,
/// ...
}So we can work with the proper fungible asset type and can avoid these low-level operations?
The getters would just become:
pub fn requested_asset(&self) -> Asset {
self.requested_asset.into()
}
pub fn requested_faucet_id(&self) -> AccountId {
self.requested_asset.faucet_id()
}
pub fn requested_asset_amount(&self) -> u64 {
self.requested_asset.amount()
}Though not sure we need the id and amount getters, since it's easy to do storage.requested_asset().faucet_id(), etc.
This would also make the PswapNoteStorage builder type-safer (since it would not take Words as inputs anymore).
| pub fn execute( | ||
| &self, | ||
| consumer_account_id: AccountId, | ||
| input_amount: u64, | ||
| inflight_amount: u64, | ||
| ) -> Result<(Note, Option<PswapNote>), NoteError> { |
There was a problem hiding this comment.
Is this intended to be used in production or is this for testing purposes?
If the goal is just testing, this seems fine, but then I'd move it behind #[cfg(any(feature = "testing", test))].
If it's for production, I'd change the input and inflight amounts to FungibleAsset, so the function has a guarantee that the input amounts do not exceed FungibleAsset::MAX_AMOUNT and that the function can check whether the faucet ID matches the requested faucet ID (which it currently assumes, but does not validate).
There was a problem hiding this comment.
Yes, it is intended to be used by dex operators or market makers who are going to deal with Pswap notes. Instead of them writing their own execute code to create the notes, they can directly import it.
Okay, I will take the complete FungibleAsset as input in the code wherever I am taking asset_amount as input and creating the asset in the function.
| pub fn calculate_output_amount( | ||
| offered_total: u64, | ||
| requested_total: u64, | ||
| input_amount: u64, | ||
| ) -> u64 { |
There was a problem hiding this comment.
If this took &self it'd b equivalent to calculate_offered_for_requested - do we need both functions in the public API?
There was a problem hiding this comment.
I just added this function with the thought process that any dex operator might require it as helping function.But you are right they can simply call calculate_offered_for_requested , we might not need this as public.
| let remainder_serial_num = Word::from([ | ||
| Felt::new(self.serial_number[0].as_canonical_u64() + 1), | ||
| self.serial_number[1], | ||
| self.serial_number[2], | ||
| self.serial_number[3], | ||
| ]); | ||
|
|
||
| let attachment_word = Word::from([Felt::new(offered_amount_for_fill), ZERO, ZERO, ZERO]); | ||
| let attachment = NoteAttachment::new_word(NoteAttachmentScheme::none(), attachment_word); |
There was a problem hiding this comment.
Would be good to avoid Felt::new in general, but especially production code.
We can replace this with:
self.serial_number[0] + Felt::ONEand
let attachment_word = Word::from([
Felt::try_from(offered_amount_for_fill).expect("explain why this should fit"),
ZERO,
ZERO,
ZERO,
]);| fn create_remainder_pswap_note( | ||
| &self, | ||
| consumer_account_id: AccountId, | ||
| remaining_offered_asset: Asset, | ||
| remaining_requested_amount: u64, | ||
| offered_amount_for_fill: u64, | ||
| ) -> Result<PswapNote, NoteError> { |
There was a problem hiding this comment.
With the other proposed changes to use FungibleAsset in execute, we may be able to take FungibleAsset as an input here as well so we can avoid constructing the assets internally.
| let p2id_serial_digest = Hasher::merge(&[swap_count_word, self.serial_number]); | ||
| let p2id_serial_num: Word = p2id_serial_digest; |
There was a problem hiding this comment.
| let p2id_serial_digest = Hasher::merge(&[swap_count_word, self.serial_number]); | |
| let p2id_serial_num: Word = p2id_serial_digest; | |
| let p2id_serial_num = Hasher::merge(&[swap_count_word, self.serial_number]); |
| # Calculate offered_out for input_amount | ||
| mem_load.AMT_REQUESTED_IN | ||
| mem_load.AMT_REQUESTED | ||
| mem_load.AMT_OFFERED | ||
| # => [offered, requested, input_amount] | ||
| exec.calculate_tokens_offered_for_requested | ||
| # => [input_offered_out] | ||
|
|
||
| mem_store.AMT_OFFERED_OUT_INPUT | ||
| # => [] | ||
|
|
||
| # Calculate offered_out for inflight_amount | ||
| mem_load.AMT_REQUESTED_INFLIGHT | ||
| mem_load.AMT_REQUESTED | ||
| mem_load.AMT_OFFERED | ||
| # => [offered, requested, inflight_amount] | ||
|
|
||
| exec.calculate_tokens_offered_for_requested | ||
| # => [inflight_offered_out] | ||
| mem_store.AMT_OFFERED_OUT_INFLIGHT |
There was a problem hiding this comment.
What's the advantage of calculating the input and inflight output amounts separately? We could save a few cycles if we added them together and called calculate_tokens_offered_for_requested only once? If there's a reason, it'd be great to document it in a comment here.
There was a problem hiding this comment.
We need to calculate the offered amount corresponding to the input_amount seperately, so that we can send that value to the consumer's vault.
We need to calculate the total offered out to create the Pswap note for the remaining asset.
…ve safety and docs - PswapNoteStorage: store FungibleAsset instead of raw key/value Words - PswapNote: store offered_asset as FungibleAsset instead of NoteAssets - execute(): take Option<FungibleAsset> for input/inflight assets - Split execute into execute() and execute_full_fill_network() for clarity - create_tag(): accept &FungibleAsset instead of &Asset - Eliminate all Felt::new usage (use Felt::from, Felt::try_from, + ONE) - Add swap_count overflow protection via checked_add - Add script root verification in TryFrom<&Note> - Add MASM fill validation (fill_amount <= requested) - Rename MASM constants: _INPUT -> _ITEM, SWAPP_ -> PSWAP_ - Make calculate_output_amount private, add precision doc comment - Add doc comments on all public accessors and methods - Add network account full fill test - Document network transaction compatibility throughout
Add PSWAP (partially-fillable swap) note to miden-standards — a new note script that enables partial
fills, creator reclaim, and inflight cross-swaps for decentralized asset exchange
1e5 precision factor, and an early-return optimization for full fills to avoid integer truncation
remainder), storage parsing, and calculate_offered_for_requested convenience method
reclaim, invalid input rejection, multiple fill amounts, non-exact ratios, fuzz cases, and chained
partial fills
Test plan
calculation, storage parsing)
reclaim, fuzz, chained fills)